﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using LSA.Framework.Pinvoke;

namespace LSA.Framework
{
    [Serializable]
    public unsafe class LsaSpace
    {
        // TODO: wordId exceptions
        public Dictionary<string, bool> StopWords { get; private set; }

        public Dictionary<string, byte> Entities { get; set; }
        
        [NonSerialized]
        private SvdResult* svdResult;

        private FrequencyMatrixBuilder matrixBuilder;

        [NonSerialized]
        private double** termByTermMatrix = null;

        [NonSerialized]
        private double** documentByDocumentMatrix = null;

        public string[] FileNames { get; private set; }

        [NonSerialized]
        private int* wordsClusterization;

        [NonSerialized]
        private double** wordsClusterCenters;

        private Dictionary<int, int> entitiesClusterCache; 

        private int wordsClusterCount = 0;

        private int diSerialized;

        public int WordsCount { get { return matrixBuilder.WordsList.Count; } }
        public int DocumentsCount { get { return matrixBuilder.DocumentsCount; } }
        public int Dimension { get { return svdResult->d; } }

        public IEnumerable<WordInfo> AllWords
        {
            get
            {
                return matrixBuilder.WordsList.OrderByDescending(x => matrixBuilder.WordsFrequency.Sum(y =>
                                                                    {
                                                                        int ret = 0;
                                                                        y.TryGetValue(x.Value, out ret);
                                                                        return ret;
                                                                    })).
                    OrderByDescending(x => IsWordEntity(x.Value)).Select(
                        x =>
                        new WordInfo
                            {
                                Id = x.Value,
                                IsEntity = IsWordEntity(x.Value),
                                Word = IsWordEntity(x.Value) ? GetEntityString(x.Key) : x.Key,
                                TotalCount = matrixBuilder.WordsFrequency.Sum(y =>
                                                                    {
                                                                        int ret = 0;
                                                                        y.TryGetValue(x.Value, out ret);
                                                                        return ret;
                                                                    })
                            });
            }
        }

        public IEnumerable<string> AllDocuments
        {
            get { return FileNames.Select(x => x.Split('\\').Last()); }
        }

        public bool IsBulgarian = true;

        /// <summary>
        /// Searchs the documents from the courpus for a particular word with <param name=" wordId" /> id <param name=" wordId" />.
        /// </summary>
        /// <param name="wordId">ID of the searched word</param>
        /// <param name="withEntities">Could wordId be an entity?</param>
        /// <returns>List of the documents' filepaths.</returns>
        public List<int> DocumentsWithWord(int wordId, bool withEntities)
        {
            List<int> files = new List<int>();

            if (IsWordEntity(wordId))
            {
                
            }

            for (int i = 0; i < matrixBuilder.WordsFrequency.Length; i++)
            {
                if (matrixBuilder.WordsFrequency[i].ContainsKey(wordId))
                {
                    files.Add(i);
                }
            }
            return files;
        }

        public List<int> DocumentsWithWord(int wordId)
        {
            List<int> files = new List<int>();
            for (int i = 0; i < matrixBuilder.WordsFrequency.Length; i++)
            {
                if (matrixBuilder.WordsFrequency[i].ContainsKey(wordId))
                {
                    files.Add(i);
                }
            }
            return files;
        }

        public int WordsPerDocument(int documentId)
        {
            if (documentId < 0 || documentId >= DocumentsCount)
            {
                throw new ArgumentOutOfRangeException("documentId", "Invalid documentId");
            }
            return matrixBuilder.WordsPerDocument[documentId];
        }

        public double[] Get2dWordCoordinates(int wordId, int d1, int d2)
        {
            if (wordId < 0 || wordId >= WordsCount)
            {
                throw new ArgumentOutOfRangeException("wordId", "Invalid wordId");
            }
            if (d1 >= Dimension)
            {
                throw new ArgumentOutOfRangeException("d1", "The dimension you have selected is greater than the matrix`s dimension.");
            }
            if (d2 >= Dimension)
            {
                throw new ArgumentOutOfRangeException("d2", "The dimension you have selected is greater than the matrix`s dimension.");
            }
            return new[]{termByTermMatrix[wordId][d1], termByTermMatrix[wordId][d2]};
        }

        public double GetWordCoordinate(int wordId, int d)
        {
            if (wordId < 0 || wordId >= WordsCount)
            {
                throw new ArgumentOutOfRangeException("wordId", "Invalid wordId");
                return 0;
            }
            if (d >= Dimension || d < 0)
            {
                throw new ArgumentOutOfRangeException("d", "The dimension you have selected is greater than the matrix`s dimension.");
                return 0;
            }
            return termByTermMatrix[wordId][d];
        }

        public int GetWordId(string word)
        {
            return GetWordId(word, false);
        }

        public int GetWordId(string word, bool withEntities)
        {
            int id;
            if(withEntities)
            {
                if (Entities.ContainsKey(word))
                {
                    for(int i = 0; i < Entities.Count; i++)
                    {
                        if (Entities.ElementAt(i).Key == word)
                        {
                            if (matrixBuilder.WordsList.TryGetValue(i.ToString(), out id))
                                return id;
                        }
                    }
                }
            }
            if (matrixBuilder.WordsList.TryGetValue(word, out id))
            {
                return id;
            }
            throw new ArgumentOutOfRangeException(word);
        }

        public string GetWordString(int wordId)
        {
            return GetWordString(wordId, false);
        }

        public string GetWordString(int wordId, bool withEntities)
        {
            if (wordId < 0 || wordId >= WordsCount)
            {
                throw new ArgumentOutOfRangeException("wordId", "Invalid wordId");
            }
            if (IsWordEntity(wordId))
            {
                return GetEntityString(matrixBuilder.WordsList.ElementAt(wordId).Key);
            }
            return matrixBuilder.WordsList.ElementAt(wordId).Key;
        }

        public string GetEntityString(string word)
        {
            try
            {
                return Entities.ElementAt(int.Parse(word)).Key;
            }
            catch (FormatException)
            {
                throw new ArgumentOutOfRangeException("word", "Invalid word");
            }
            catch (ArgumentOutOfRangeException)
            {
                throw new ArgumentOutOfRangeException("word", "Invalid word");
            }
        }

        public bool IsWordEntity(int wordId)
        {
            return matrixBuilder.EntitiesCache.ContainsKey(wordId);
        }
        
        public List<WordRelationPair>[] GetAllWordsClusters(int perCluster)
        {
            if(wordsClusterCount == 0)
            {
                throw new ArgumentException("You must first cluster words.");
            }

            List<WordRelationPair>[] words = new List<WordRelationPair>[wordsClusterCount];
            for (int i = 0; i < wordsClusterCount; i++)
            {
                if (matrixBuilder.EntitiesCache.Count == 0)
                {
                    words[i] = Enumerable.Range(0, WordsCount).AsParallel()
                        .Where(x => (wordsClusterization[x] == i))
                        .Select(index => new WordRelationPair
                            {
                                Word = BuildWordInfo(index),
                                Relation = EuclideanDistanceBetweenVectors(wordsClusterCenters[i], termByTermMatrix[index], Dimension)
                            })
                        .OrderBy(x => x.Relation)
                        .Take(perCluster).ToList();
                }
                else
                {
                    words[i] = Enumerable.Range(0, WordsCount).AsParallel()
                        .Where(x => matrixBuilder.EntitiesCache.ContainsKey(x))
                        .Where(x => (wordsClusterization[entitiesClusterCache[x]] == i))
                        .Select(index => new WordRelationPair
                            {
                                Word = BuildWordInfo(index),
                                Relation = EuclideanDistanceBetweenVectors(wordsClusterCenters[i], termByTermMatrix[index], Dimension)
                            })
                        .OrderBy(x => x.Relation)
                        .Take(perCluster).ToList();
                }
            }

            return words;
        }

        public WordInfo BuildWordInfo(int wordId)
        {
            WordInfo word = new WordInfo {Id = wordId, IsEntity = IsWordEntity(wordId), TotalCount = WordInfo.NaN};
            word.Word = GetWordString(wordId, word.IsEntity);

            return word;
        }

        /// <summary>
        /// Clusterizes the words in n grousps
        /// </summary>
        /// <param name="clusters">The number of clusters</param>
        public void KMeansClusterWords(int clusters)
        {
            wordsClusterCount = clusters;
            entitiesClusterCache = new Dictionary<int, int>();

            if (matrixBuilder.EntitiesCache.Count > 0)
            {
                wordsClusterization = CppInvoker.CreateNew1dArray(matrixBuilder.EntitiesCache.Count);
                double** smallTermbyTerm = CppInvoker.CreateNewArray(matrixBuilder.EntitiesCache.Count, Dimension);

                int i = 0;
                foreach (KeyValuePair<int, bool> pair in matrixBuilder.EntitiesCache)
                {
                    for (int j = 0; j < Dimension; j++)
                    {
                        smallTermbyTerm[i][j] = termByTermMatrix[pair.Key][j];
                        entitiesClusterCache[pair.Key] = i;
                    }
                    i++;
                }

                wordsClusterCenters = CppInvoker.KMeans(smallTermbyTerm, matrixBuilder.EntitiesCache.Count, Dimension, clusters, wordsClusterization);
            }
            else
            {
                wordsClusterization = CppInvoker.CreateNew1dArray(matrixBuilder.WordsList.Count);

                wordsClusterCenters = CppInvoker.KMeans(termByTermMatrix, matrixBuilder.WordsList.Count, Dimension, clusters, wordsClusterization);
            }
        }

        /// <summary>
        /// Returns the clusterId of a word.
        /// </summary>
        /// <param name="wordId">The word, which cluster you are looking for.</param>
        /// <returns>The clusterId</returns>
        public int GetWordClusterBelonging(int wordId)
        {
            if (wordsClusterization == null)
            {
                throw new ArgumentException("You must first cluster the words.");
            }
            if (wordId < 0 || wordId >= WordsCount)
            {
                throw new ArgumentOutOfRangeException("wordId", "Invalid wordId");
            }

            return wordsClusterization[wordId];
        }

        /// <summary>
        /// Gets the first <param name="limit" /> words mostly related to <param name="wordId" />.
        /// </summary>
        /// <param name="wordId">The word whose neighbours are searched.</param>
        /// <param name="limit">The limit of words.</param>
        /// <param name="showOnlyEntities">Should the function return only entities?</param>
        /// <returns></returns>
        public List<WordRelationPair> GetKnnOfWord(int wordId, int limit, bool showOnlyEntities)
        {
            if (wordId < 0 || wordId >= WordsCount)
            {
                throw new ArgumentOutOfRangeException("wordId", "Invalid wordId");
            }

            var topIndexes = Enumerable.Range(0, WordsCount).AsParallel()
                .Where(x => x != wordId)
                .Where(x => !showOnlyEntities || (matrixBuilder.EntitiesCache.ContainsKey(x)))
                .OrderByDescending(x => PearsonCorrelationBetweenVectors(termByTermMatrix[wordId], termByTermMatrix[x], Dimension))
                .Take(limit)
                .Select(index => new WordRelationPair
                                     {
                                         Word = BuildWordInfo(index), 
                                         Relation = PearsonCorrelationBetweenVectors(termByTermMatrix[wordId], termByTermMatrix[index], Dimension)
                                     })
                .OrderBy(x => Guid.NewGuid())
                .ToList();
            return topIndexes;
        }

        private void CalculateTermByTermMatrix()
        {
            termByTermMatrix = CppInvoker.CreateNewArray(WordsCount, Dimension);
            CppInvoker.MatrixMultiplication(WordsCount, Dimension, Dimension,
                                            CppInvoker.TransposeDenseMatrix(svdResult->Ut)->value,
                                            svdResult->GetSingularValues()->value, termByTermMatrix);
        }

        private void CalculateDocumentByDocumentMatrix()
        {
            documentByDocumentMatrix = CppInvoker.CreateNewArray(DocumentsCount, Dimension);
            CppInvoker.MatrixMultiplication(Dimension, Dimension, DocumentsCount,
                                            svdResult->GetSingularValues()->value, svdResult->GetVt().value,
                                            documentByDocumentMatrix);

            CppInvoker.TransposeDenseMatrix(documentByDocumentMatrix, DocumentsCount, Dimension);
        }

        public double RelationBetweenWords(int wordId, int word2Id)
        {
            if (wordId < 0 || wordId >= WordsCount)
            {
                throw new ArgumentOutOfRangeException("wordId", "Invalid wordId");
            }
            if (word2Id < 0 || word2Id >= WordsCount)
            {
                throw new ArgumentOutOfRangeException("word2Id", "Invalid wordId");
            }

            double relation = PearsonCorrelationBetweenVectors(termByTermMatrix[wordId], termByTermMatrix[word2Id], Dimension);
            return relation;
        }

        public double RelationBetweenWords(string word, string word2)
        {
            int wordId = GetWordId(word);
            int word2Id = GetWordId(word2);
            return RelationBetweenWords(wordId, word2Id);
        }

        public double RelationBetweenDocuments(int documentId, int document2Id)
        {
            if (documentId < 0 || documentId >= DocumentsCount)
            {
                throw new ArgumentOutOfRangeException("documentId", "Invalid documentId");
            }
            if (document2Id < 0 || document2Id >= DocumentsCount)
            {
                throw new ArgumentOutOfRangeException("document2Id", "Invalid documentId");
            }

            double relation = CosineSimilarityOfTwoVectors(documentByDocumentMatrix[documentId], documentByDocumentMatrix[document2Id], Dimension);
            return relation;
        }

        private static double CosineSimilarityOfTwoVectors(double* vector1, double* vector2, int dimentions)
        {
            double dotPr = 0;
            double vect1Len = 0;
            double vect2Len = 0;
            for(int i = 0; i < dimentions; i++)
            {
                dotPr += vector1[i] * vector2[i];
                vect1Len += vector1[i] * vector1[i];
                vect2Len += vector2[i] * vector2[i];
            }
            if (vect1Len == 0.0 || vect2Len == 0.0) return 0;
            return dotPr/(Math.Sqrt(vect1Len)*Math.Sqrt(vect2Len));
        }

        private double EuclideanDistanceBetweenVectors(double* vector1, double* vector2, int dimensions)
        {
            double sum = 0;
            for (int i = 0; i < dimensions; i++)
            {
                sum += (vector1[i] - vector2[i]) * (vector1[i] - vector2[i]);
            }
            return sum;
        }

        private double PearsonCorrelationBetweenVectors(double* vector1, double* vector2, int dimensions)
        {
            double vector1Average = Enumerable.Range(0, dimensions).Average(i => vector1[i]);
            double vector2Average = Enumerable.Range(0, dimensions).Average(i => vector2[i]);

            double sum1 = 0, sum2 = 0, sum3 = 0;
            for (int i = 0; i < dimensions; i++)
            {
                sum1 += (vector1[i] - vector1Average) * (vector2[i] - vector2Average);
                sum2 += (vector1[i] - vector1Average) * (vector1[i] - vector1Average);
                sum3 += (vector2[i] - vector2Average) * (vector2[i] - vector2Average);
            }
            if (sum1 == 0.0 || sum2 == 0.0 || sum3 == 0.0) return 0.0;
            double result = sum1 / (Math.Sqrt(sum2) * Math.Sqrt(sum3));
            return result;
        }

        public LsaSpace(string[] fileNames, bool tfIdf, bool stem, string[] stopWords, Dictionary<string, byte> entitiesParam, int dimensions)
        {
            if(fileNames.Length < 2)
            {
                throw new ArgumentException("No files selected or just one");
            }

            StopWords = new Dictionary<string, bool>();
            foreach (string word in stopWords)
            {
                if (word == null)
                {
                    continue;
                }
                StopWords.Add(word, true);
            }

            Entities = entitiesParam ?? new Dictionary<string, byte>();
            string[] entities = Entities.Keys.ToArray();

            matrixBuilder = new FrequencyMatrixBuilder(fileNames, tfIdf, stem, StopWords, entities);
            svdResult = CppInvoker.PerformSvd(matrixBuilder.Matrix, (dimensions > fileNames.Length) ? 0 : dimensions);
            FileNames = fileNames;

            CalculateTermByTermMatrix();
            CalculateDocumentByDocumentMatrix();
        }

        [OnSerializing]
        internal void OnSerializingMethod(StreamingContext context)
        {
            diSerialized = Dimension;
        }

        [OnDeserialized]
        internal void OnDeserializedMethod(StreamingContext context)
        {
            matrixBuilder.OnDeserializedMethod(context);
            svdResult = CppInvoker.PerformSvd(matrixBuilder.Matrix, diSerialized);
            CalculateTermByTermMatrix();
            CalculateDocumentByDocumentMatrix();

            if (wordsClusterCount == 0) return;
            wordsClusterization = CppInvoker.CreateNew1dArray(WordsCount);
            wordsClusterCenters = CppInvoker.KMeans(termByTermMatrix, WordsCount, diSerialized, wordsClusterCount, wordsClusterization);
        }
    }
}
